先前Spring boot MVC 應用系統設計前準備 https://ithelp.ithome.com.tw/articles/10398853 ,有提及開發前後端系統程式專案結構,這裡先依Spring boot 啟動過程中,加入一些系統啟動時準備資料(例如menu資料),
完成登入並Landing顯示功能選項的功能開發,因為不擅長前端版面開發,所以藉由經Bootstrap以及Jquery包裝設計好的Free Bootstrap Admin Template - AdminLTE 來學習與開發前端Hhml功能,前端採用Thymeleaf的原因,是因為其語法幾乎與 Html語法相同,所有Hhtml都很容易轉換為Thymeleaf的樣板。很容易轉換為Thymeleaf的樣板。
**AdminLTE 參考 URL: https://adminlte.io/ **
**Thymeleaf 參考 URL: https://www.thymeleaf.org/documentation.html **
位置 package tw.lewishome.webapp;
SystemServletContextListener.java (new 系統啟動相關程序)SystemServletContextListener 再啟動 SystemApplicationListener。package tw.lewishome.webapp;
import org.springframework.stereotype.Component;
import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import lombok.extern.slf4j.Slf4j;
/**
* 系統 ServletContextListener,於 Web 應用程式啟動時執行初始化邏輯。
*
* 主要功能:
* 設定系統執行環境(Profile),並初始化系統參數。</li>
*
* 注意事項:
* 在初始化任何 Filter 或 Servlet 之前執行。</li>
*
* @author Lewis
*/
@Component
@Slf4j
public class SystemServletContextListener implements ServletContextListener {
/**
* Fix for javadoc warning :
* use of default constructor, which does not provide a comment
* Constructs a new SystemServletContextListener instance.
* This is the default constructor, implicitly provided by the compiler
* and can be used to create a new instance of the class.
*/
public SystemServletContextListener() {
// Constructor body (can be empty)
}
/**
* <pre>
* 在初始化Web Application 的任何Flitter或Servlet之前,將通知ServletContextListener初始化。
* Jboss 先啟動 SystemApplicationListener 再啟動 SystemServletContextListener
* spring boot 先啟動 SystemServletContextListener 再啟動 SystemApplicationListener
*
* Junit Test 啟動 @SpringBootTest 時不會啟動此程序
* </pre>
*/
@Override
public void contextInitialized(ServletContextEvent sce) {
// 配合Spring AutoConfiguration, 若有加 @Autowired JPA repository,則DataBase會先啟動
// 否則Database 沒有啟動。(於 JNDI已經有連線時,沒有影響)
log.info("啟動時自動執行一次 ServletContextListener 的 contextInitialized 方法");
}
}
SystemApplicationListener.java (new 系統啟動程序)SystemApplicationListener 再啟動 SystemServletContextListenerpackage tw.lewishome.webapp;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import lombok.extern.slf4j.Slf4j;
import tw.lewishome.webapp.base.cache.caffeine.CaffeineCacheUtils;
import tw.lewishome.webapp.base.cache.redis.RedisCacheUtils;
import tw.lewishome.webapp.base.schedule.ScheduleTaskService;
import tw.lewishome.webapp.base.utility.common.CommUtils;
import tw.lewishome.webapp.base.utility.common.SystemEnvReader;
import tw.lewishome.webapp.page.PageConstants;
import tw.lewishome.webapp.page.base.service.PageMenuItemService;
/**
* 在 Web 應用程式初始化完成時執行的監聽器。
*
* Jboss/Tomcat 會先啟動 SystemApplicationListener,再啟動 SystemServletContextListener;
* Spring Boot 則是先啟動 SystemServletContextListener,再啟動 SystemApplicationListener。
* <br>
* 特別注意:Junit 測試啟動 @SpringBootTest 時,會執行此 SystemApplicationListener。
*
* <ul>
* <li>收集所有系統 Endpoint 並存入常數。</li>
* <li>初始化系統選單項目。</li>
*
* </ul>
*
* @author lewis
*/
@Component
@Slf4j
public class SystemApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
/**
*
* Fix for javadoc warning :
* use of default constructor, which does not provide a comment
*
* Constructs a new SystemApplicationListener instance.
* This is the default constructor, implicitly provided by the compiler
*/
public SystemApplicationListener() {
// Constructor body (can be empty)
}
@Autowired
ScheduleTaskService scheduleTaskService;
@Autowired
SystemEnvReader systemEnvReader;
@Autowired
PageMenuItemService pageMenuItemService;
@Autowired
CaffeineCacheUtils caffeineCacheUtils;
@Autowired
RedisCacheUtils redisCacheUtils;
/**
*
*
* 啟動時自動執行一次 SystemApplicationListener 的 onApplicationEvent 方法。
*
* 主要功能:
* <ul>
* <li>收集所有 Endpoint 並存入 BaseSystemConstants.ListSysEndPoint。</li>
* <li>初始化 BasePageConstants.pageMenuItems。</li>
* <li>檢查並產生全域 RSA 金鑰。</li>
* </ul>
*/
@Override
public void onApplicationEvent(@NonNull ContextRefreshedEvent event) {
// 配合Spring AutoConfiguration, 若有加 @Autowired JPA repository,則DataBase會先啟動
// 否則Database 沒有啟動。(於 Jboss執行時,JNDI已經有連線,沒有影響)
log.info("啟動時自動執行一次 SystemApplicationListener 的 onApplicationEvent 方法");
// 取得系統所有的 URL Endpoint
List<String> listEndpoint = new ArrayList<>();
ApplicationContext applicationContext = event.getApplicationContext();
RequestMappingHandlerMapping requestMappingHandlerMapping = applicationContext
.getBean("requestMappingHandlerMapping", RequestMappingHandlerMapping.class);
Map<RequestMappingInfo, HandlerMethod> map = requestMappingHandlerMapping
.getHandlerMethods();
log.info( "======= System Endpoint List ========== " );
map.forEach((key, value) -> {
log.info("Key:{} = Value: {}", key, value);
String keyString = key.toString();
String oneListEndPoint = keyString.substring(keyString.indexOf('[') + 1, keyString.indexOf(']'));
List<String> splitOneListEndPoint = CommUtils.splitDelimiter(oneListEndPoint, "||");
for (int i = 0; i < splitOneListEndPoint.size(); i++) {
if (Boolean.FALSE.equals(listEndpoint.contains(splitOneListEndPoint.get(i)))) {
listEndpoint.add(splitOneListEndPoint.get(i));
}
}
});
log.info( "======= System Endpoint List end ========== " );
// 將 系統所有的 ENDPOINT 加入系統常變數 listSysEndPoint
String contextPath = event.getApplicationContext().getApplicationName();
listEndpoint.forEach(x -> {
GlobalConstants.LIST_SYSTEM_ENDPOINT.add(contextPath + x);
});
// 設定環境變數於 GlobalConstants.ENV_VAR//
log.info("設定環境變數於 GlobalConstants.ENV_VAR ");
GlobalConstants.ENV_VAR = systemEnvReader.getAllEnvironmentVariables();
log.info( "======= System Environment Value List ========== " );
GlobalConstants.ENV_VAR.forEach((key, value) -> {
log.info("Key:{} = Value: {}", key, value);
});
log.info( "======= System Environment Value List ========== " );
// 顯示系統排程設定
scheduleTaskService.listScheduledTaskJobs();
// 清除所有系統快取 caffeineCache
caffeineCacheUtils.evictAllCaches();
log.info("清除 所有系統快取完成");
//只有主伺服器才清除 Redis 快取
if (scheduleTaskService.isScheduleMainServer(GlobalConstants.HOST_SERVER_NAME)) {
redisCacheUtils.evictAllCaches();
log.info("清除 Redis 所有系統快取完成");
}
// 初始化系統選單項目
PageConstants.pageMenuItems = pageMenuItemService.getPageMenuItems();
log.info("初始化系統選單項目完成");
}
}
3.SystemApplicationStartingEvent.java (new 系統啟動程序)
package tw.lewishome.webapp;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
/**
* 應用程式啟動事件監聽器。
* <p>
* 此類別用於監聽 Spring Boot 應用程式的啟動事件,
* 並在應用程式完全啟動後執行相應的初始化邏輯。
* </p>
*
* @author Lewis
* @since 1.0
*/
@Component
@Slf4j
public class ApplicationStartingEvent {
/**
* 應用程式就緒事件的回調方法。
* 當 Spring Boot 應用程式完全啟動並且所有元件都已初始化時被調用。
* 此方法可用於執行應用程式啟動後的初始化作業。
*
* @see org.springframework.boot.context.event.ApplicationReadyEvent
*/
@EventListener(ApplicationReadyEvent.class)
public void onApplicationReady() {
log.info("Application started successfully!");
}
}
位置 tw.lewishome.webapp.database.primary.entity;
SysMenuDataEntity.java (new 系統選單Menu Entity Table)package tw.lewishome.webapp.database.primary.entity;
import java.io.Serializable;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.UUID;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.Index;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import tw.lewishome.webapp.database.audit.EntityAudit;
//https://layui.dev/docs/2/base.html
//https://www.ombudsman.go.id/assets/js/plugins/treetable/
/**
* SysMenuData Table Entity
*
* @author lewis
* @version $Id: $Id
*/
@Entity
// 資料庫的 Table 名稱
// @Table(name = "sysmenudata", indexes = {
// @Index(name = "idx_parentUuid", columnList = "menuParentUuid,menuSeq"),
// @Index(name = "idx_lastname_firstname", columnList = "lastName, firstName")})
@Table(name = "sysmenudata", indexes = @Index(name = "idx_parentUuid", columnList = "menuParentUuid,menuSeq"))
@Data
@EqualsAndHashCode(callSuper = true)
@AllArgsConstructor
public class SysMenuDataEntity extends EntityAudit<String> {
/**
* Fix for javadoc warning :
* use of default constructor, which does not provide a comment
* Constructs a new SysMenuDataEntity instance.
* This is the default constructor, implicitly provided by the compiler
* if no other constructors are defined.
*/
public SysMenuDataEntity() {
// Constructor body (can be empty)
}
private static final long serialVersionUID = 1L;
/** Primary Key */
@EmbeddedId
public DataKey dataKey;
/** Menu Seq */
@Column(name = "menuSeq")
public int menuSeq;
/** isMenu flag */
@Column(name = "isMenu")
public Boolean isMenu;
/** Menu item description */
@Column(name = "menuDesc", length = 64)
public String menuDesc;
/** Menu Function URL (for Do Menu) */
@Column(name = "menuUrl", length = 256)
public String menuUrl;
/** Menu Function (BasePageConstants.ListAvailPageFunc) */
@Column(name = "menuFunc", length = 32)
public String menuFunc;
/** 存取權限 */
@Column(name = "menuRoles", length = 512)
public String menuRoles;
/** 上一層節點Uuid */
@Column(name = "menuParentUuid", length = 1024)
public String menuParentUuid;
/**
* Entity Key
*
*/
@Embeddable
@Data
public static class DataKey implements Serializable {
/**
* Fix for javadoc warning :
* use of default constructor, which does not provide a comment
* Constructs a new DataKey instance.
* This is the default constructor, implicitly provided by the compiler
* if no other constructors are defined.
*/
public DataKey() {
// Constructor body (can be empty)
}
private static final long serialVersionUID = 1L;
/** UUID */
@Column(name = "uuid", length = 64)
public String uuid = UUID.randomUUID().toString();
}
/**
* MyBatis TypeHandler for DataKey
*/
/**
* MyBatis BaseTypeHandler} for converting between the application's
* DataKey object and its JDBC representation.
*
*/
public static class DataKeyHandler extends BaseTypeHandler<DataKey> {
/**
* Fix for javadoc warning :
* use of default constructor, which does not provide a comment
* Constructs a new AsyncServiceWorkerSample instance.
* This is the default constructor, implicitly provided by the compiler
* if no other constructors are defined.
*/
public DataKeyHandler() {
// Constructor body (can be empty)
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, DataKey parameter, JdbcType jdbcType)
throws SQLException {
try {
ps.setString(1, parameter.getUuid());
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public DataKey getNullableResult(ResultSet rs, String columnName) throws SQLException {
DataKey dataKey = new DataKey();
if (rs.wasNull() == false) {
dataKey.setUuid(rs.getString("uuid"));
}
return dataKey;
}
@Override
public DataKey getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
DataKey dataKey = new DataKey();
if (rs.wasNull() == false) {
dataKey.setUuid(rs.getString(1));
}
return dataKey;
}
@Override
public DataKey getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
DataKey dataKey = new DataKey();
if (cs.wasNull() == false) {
dataKey.setUuid(cs.getString(1));
}
return dataKey;
}
}
}
位置 tw.lewishome.webapp.database.primary.repository;
SysMenuDataRepository.java (new 系統選單Menu repository存取)package tw.lewishome.webapp.database.primary.repository;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import tw.lewishome.webapp.database.primary.entity.SysMenuDataEntity;
/**
* SysMenuData JPA Repository
*
* @author lewis
* @version $Id: $Id
*/
@Transactional
@Repository
public interface SysMenuDataRepository extends JpaRepository<SysMenuDataEntity, SysMenuDataEntity.DataKey>,
JpaSpecificationExecutor<SysMenuDataEntity> {
/**
*
* findByMenuParentUuid.
*
*
* @param menuParentUuid a String object
* @return a List object
*/
public List<SysMenuDataEntity> findByMenuParentUuid(String menuParentUuid);
}
位置 package tw.lewishome.webapp.page.base.model
PageMenuItemModel.java (new 系統選單Menu共用物件)
package tw.lewishome.webapp.page.base.model;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
/**
* <pre>
* 系統選單(Menu Model),使用遞廻方式。
* </pre>
*
* @author lewis
* @version $Id: $Id
*/
@Data
public class PageMenuItemModel implements Serializable {
/**
* Fix for javadoc warning :
* use of default constructor, which does not provide a comment
* Constructs a new PageMenuItemModel instance.
* This is the default constructor, implicitly provided by the compiler
* if no other constructors are defined.
*/
public PageMenuItemModel() {
// Constructor body (can be empty)
}
/** serialVersionUID */
private static final long serialVersionUID = 1L;
/** 選單抬頭 文字 */
private String menuBarText = "";
/** 選單 Model */
private List<MenuItems> naviItems = new ArrayList<MenuItems>();
/**
* 系統選單資料
**/
@Data
public static class MenuItems implements Serializable {
/**
* Fix for javadoc warning :
* use of default constructor, which does not provide a comment
* Constructs a new MenuItems instance.
* This is the default constructor, implicitly provided by the compiler
* if no other constructors are defined.
*
*/
public MenuItems() {
// Constructor body (can be empty)
}
/** serialVersionUID */
private static final long serialVersionUID = 1L;
/** 選單順序 */
public int menuSeq = 0;
/** 選單順序 */
public Boolean isMenu = true;
/** 是否為選單 ( false: 是程式) */
public String menuDesc = "";
/** 程式 URL (Controllr 處理程序 isMenu=false時必須指定) */
public String menuUrl = "";
/** 選單功能 "Add" "Edit" "Confirm" "Inquiry" ("" = 不區分功能, 覆核功能需要有"Confirm" ) */
public String menuFunc = "";
/** 授權使用角色 (All /IT_Admin/Sys_Admin/User_Admin/User_Id) */
public String menuRoles = "";
/** 子選單或程式 (摺疊) */
public List<MenuItems> collapseItems = new ArrayList<MenuItems>();
}
}
位置 package tw.lewishome.webapp.page.base.service;
PageMenuService.java (new 取得前端系統選單Menu資料)package tw.lewishome.webapp.page.base.service;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import tw.lewishome.webapp.GlobalConstants;
import tw.lewishome.webapp.database.primary.entity.SysMenuDataEntity;
import tw.lewishome.webapp.database.primary.repository.SysMenuDataRepository;
import tw.lewishome.webapp.database.primary.service.GetSysParmValueService;
import tw.lewishome.webapp.base.utility.common.CommUtils;
import tw.lewishome.webapp.page.base.model.PageMenuItemModel;
import tw.lewishome.webapp.page.PageConstants;
/**
* <pre>
* SystemGetMenuItemService 負責處理系統選單(Menu)相關的服務邏輯。
*
* 主要功能包含:
* 1. 根據使用者權限取得對應的選單項目,並支援快取以提升效能。
* 2. 支援從外部檔案(menuItemsData.json)或專案資源載入選單結構,方便選單維護與部署。
* 3. 提供選單資料與資料庫(SysMenuDataEntity)之間的轉換功能,支援選單資料的持久化與還原。
* 4. 根據使用者名稱與權限,動態產生符合權限的選單結構。
* 5. 支援特殊權限角色(如 IT_ADMIN、SYS_ADMIN 等)的判斷與加入。
*
* 典型使用情境:
* - 使用者登入後,根據其角色權限取得對應的選單項目。
* - 系統管理員可透過外部檔案或資料庫調整選單結構,無須重新部署應用程式。
*
* 注意事項:
* - 本服務使用 Spring Cache 機制,請確保快取設定正確。
* - 選單資料結構為遞迴巢狀,請注意資料格式正確性。
* </pre>
*
* @author Lewis
* @version 1.0
* @since 2024-06
*/
@Slf4j
@Service
public class PageMenuItemService {
/**
* Fix for javadoc warning :
* use of default constructor, which does not provide a comment
*
* Constructs a new SystemMenuItemService instance.
* This is the default constructor, implicitly provided by the compiler
*/
public PageMenuItemService() {
// Constructor body (can be empty)
}
@Autowired
private SysMenuDataRepository sysMenuDataRepository;
@Autowired
GetSysParmValueService getSysParmValueService;
/**
*
* <pre>
* 取得 menuItemsData.json的資料後
* 設定給 MenuItem Model (也可以考慮設計為讀取 DB資料)
*
* 使用了Catch功能,所以每個使用者依權限,只會準備一次Menu 。
* </pre>
*
* @param username user name
* @return Base MenuItems Model
*/
// @Cacheable(cacheNames = "catchUserMenuItems", key = "#username", cacheManager = "caffeineCacheManager")
@Caching(cacheable = {
@Cacheable(cacheNames = "catchUserMenuItems", key = "#username", cacheManager = "caffeineCacheManager"),
@Cacheable(cacheNames = "catchUserMenuItems", key = "#username", cacheManager = "redisCacheManager")
})
public PageMenuItemModel getUserMenuItems(String username) {
PageMenuItemModel pageMenuItems = PageConstants.pageMenuItems;
PageMenuItemModel userMenuItems = new PageMenuItemModel();
if (StringUtils.isBlank(username) || "anonymousUser".equalsIgnoreCase(username)) {
return userMenuItems;
}
// 取得 User Roles (包含特殊權限)
List<String> userRoles = getUserRoles(username);
// 設定 UserMenu的 BarText
userMenuItems.setMenuBarText(pageMenuItems.getMenuBarText());
// 第一層 NaviMenuItems Loop
List<PageMenuItemModel.MenuItems> listUserNaviItems = new ArrayList<>();
pageMenuItems.getNaviItems().forEach(x -> {
PageMenuItemModel.MenuItems oneNaviItems = x; // 處理第一層Menu
List<String> listNaviMenuRoles = CommUtils.splitDelimiter(oneNaviItems.getMenuRoles(), ";");
Boolean hasNaviMenuRole = userHasMenuRoles(userRoles, listNaviMenuRoles);
if (hasNaviMenuRole || "ALL".equalsIgnoreCase(oneNaviItems.getMenuRoles())) {
if (oneNaviItems.getIsMenu()) {
PageMenuItemModel.MenuItems oneUserNaviItem = getUserOneCollapseItems(userRoles, oneNaviItems);
listUserNaviItems.add(oneUserNaviItem);
} else {
listUserNaviItems.add(oneNaviItems);
}
}
});
userMenuItems.setNaviItems(listUserNaviItems);
return userMenuItems;
}
/**
* 根據使用者角色過濾並取得可用的第一層子選單項目。
*
* 此方法會遞迴處理傳入的選單項目,僅保留使用者有權限的子選單。 若子選單標記為 "ALL",則所有角色皆可存取。
*
*
* @param userRoles 使用者所擁有的角色清單
* @param oneNaviItems 第一層選單項目
* @return 過濾後僅包含使用者可存取子選單的 MenuItems 物件
*/
private PageMenuItemModel.MenuItems getUserOneCollapseItems(List<String> userRoles,
PageMenuItemModel.MenuItems oneNaviItems) {
PageMenuItemModel.MenuItems userCollapseItems = new PageMenuItemModel.MenuItems();
List<PageMenuItemModel.MenuItems> userListCollapseItems = new ArrayList<>();
oneNaviItems.getCollapseItems().forEach(x -> {
PageMenuItemModel.MenuItems oneCollapseItems = x; // 處理第一層Menu
List<String> listCollapseMenuRoles = CommUtils.splitDelimiter(oneCollapseItems.getMenuRoles(), ";");
Boolean hasCollapseMenuRole = userHasMenuRoles(userRoles, listCollapseMenuRoles);
if (hasCollapseMenuRole || "ALL".equalsIgnoreCase(oneCollapseItems.getMenuRoles())) {
if (oneCollapseItems.getIsMenu()) {
// recursive 遞迴
PageMenuItemModel.MenuItems oneUserCollapseItemsItem = getUserOneCollapseItems(userRoles,
oneCollapseItems);
userListCollapseItems.add(oneUserCollapseItemsItem);
} else {
userListCollapseItems.add(oneCollapseItems);
}
}
});
userCollapseItems.setMenuDesc(oneNaviItems.getMenuDesc());
userCollapseItems.setCollapseItems(userListCollapseItems);
return userCollapseItems;
}
// 確認 Menu Roles 有包含 User Roles 之一
private Boolean userHasMenuRoles(List<String> userRoles, List<String> listMenuRoles) {
listMenuRoles.replaceAll(String::toUpperCase);
userRoles.replaceAll(String::toUpperCase);
Boolean hasMenuRoles = false;
for (int i = 0; i < userRoles.size(); i++) {
String oneUserRole = userRoles.get(i);
if (listMenuRoles.contains(oneUserRole)) {
hasMenuRoles = true;
}
}
return hasMenuRoles;
}
/**
* 取得系統的基礎選單項目,並依照選單順序進行多層排序。
*
* 此方法會先嘗試從外部檔案取得選單資料,若失敗則從專案資源載入。取得資料後, 會對主選單(第一層)、子選單(第二層)及次子選單(第三層)依據
* {@code menuSeq} 欄位進行排序。
*
*
* 此方法結果會快取於 {@code catchUserMenuItems},快取鍵為方法名稱。
*
*
* @return 已排序的 PageMenuItemModel 物件,包含所有層級的選單項目。
* https://www.1ju.org/article/spring-two-level-cache
*/
@Caching(cacheable = {
@Cacheable(cacheNames = "catchUserMenuItems", key = "#root.methodName", cacheManager = "caffeineCacheManager"),
@Cacheable(cacheNames = "catchUserMenuItems", key = "#root.methodName", cacheManager = "redisCacheManager")
})
public PageMenuItemModel getPageMenuItems() {
// 從外部檔案取得系統 PageMenu (這樣改Menu 不需要再打包以及Deploy war)
PageMenuItemModel pageMenuItems = getPageMenuItemsExternalData();
if (pageMenuItems == null) {
// 從外部取得錯誤,再從專案Resource取得 PageMenuItem
pageMenuItems = getPageMenuItemsFromResource();
}
// Menu Item 排序 // https://www.ewdna.com/2008/10/list.html
// for 主選單 NavItems (第一層)
Collections.sort(pageMenuItems.getNaviItems(), // sort MenuItem (第一層 Menu)
new Comparator<PageMenuItemModel.MenuItems>() {
public int compare(PageMenuItemModel.MenuItems menuItem1, PageMenuItemModel.MenuItems menuItem2) {
return menuItem1.getMenuSeq() - menuItem2.getMenuSeq();
}
}); // end Collections.sort
// fro 子選單內 collapseItems (第二層)
pageMenuItems.getNaviItems().forEach(x -> {
PageMenuItemModel.MenuItems collapseItems = x;
if (collapseItems.getIsMenu()) {
Collections.sort(collapseItems.getCollapseItems(), // Sorting collapseItems (第二層 Menu)
new Comparator<PageMenuItemModel.MenuItems>() {
public int compare(PageMenuItemModel.MenuItems menuItem1,
PageMenuItemModel.MenuItems menuItem2) {
return menuItem1.getMenuSeq() - menuItem2.getMenuSeq();
}
});// end Collections.sort
// for 子選單內選單 subCollapseItems (第三層)
collapseItems.getCollapseItems().forEach(y -> {
PageMenuItemModel.MenuItems oneSubCollapseItems = y; // Sorting subCollapseItems (第三層 Menu)
if (oneSubCollapseItems.getIsMenu()) {
Collections.sort(oneSubCollapseItems.getCollapseItems(),
new Comparator<PageMenuItemModel.MenuItems>() {
public int compare(PageMenuItemModel.MenuItems menuItem1,
PageMenuItemModel.MenuItems menuItem2) {
return menuItem1.getMenuSeq() - menuItem2.getMenuSeq();
}
});// end Collections.sort
}
});
}
});
return pageMenuItems;
}
/**
* 從外部 JSON 檔案讀取系統選單資料,並轉換為 {@link PageMenuItemModel} 物件。
*
* 此方法會嘗試從指定的外部資料夾({@link GlobalConstants#EXTERNAL_FOLDER})下的
* <code>menuItemsData.json</code> 檔案載入選單資料。若檔案不存在或解析失敗,將回傳 <code>null</code>,
* 並於主控台輸出錯誤訊息。
*
*
* @return 讀取到的 {@link PageMenuItemModel} 物件,若失敗則回傳 <code>null</code>
*/
private PageMenuItemModel getPageMenuItemsExternalData() {
PageMenuItemModel rtnPageMenuItemModel = new PageMenuItemModel();
ObjectMapper objectMapper = new ObjectMapper();
String externalMenuDataFile = GlobalConstants.EXTERNAL_FOLDER + "/menuItemsData.json";
try {
// 從外部檔案取得系統 PageMenu menuItemsData (這樣改Menu 不需要在打包以及Deploy war)
rtnPageMenuItemModel = objectMapper.readValue(new File(externalMenuDataFile), PageMenuItemModel.class);
} catch (Exception ex) { // new File => FileNotFoundException or objectMapper exception
log.info("external menuItemsData {} not found" , externalMenuDataFile );
return null;
}
return rtnPageMenuItemModel;
}
/**
* 從專案的資源檔案(menuItemsData.json)讀取並解析為 PageMenuItemModel 物件。
*
* 此方法會嘗試從 classpath 下的 menuItemsData.json 檔案載入資料,並使用 Jackson 的 ObjectMapper 進行
* JSON 反序列化。如果讀取或解析過程中發生例外,將回傳 null。
*
*
* @return 解析後的 PageMenuItemModel 物件,若發生錯誤則回傳 null。
*/
private PageMenuItemModel getPageMenuItemsFromResource() {
PageMenuItemModel rtnPageMenuItemModel = new PageMenuItemModel();
ObjectMapper objectMapper = new ObjectMapper();
try {
// 從專案 Resource 檔案取得系統 PageMenu menuItemsData
rtnPageMenuItemModel = objectMapper.readValue(
getClass().getClassLoader().getResourceAsStream("menuItemsData.json"),
new TypeReference<PageMenuItemModel>() {
});
} catch (Exception ex) { // new File => FileNotFoundException or objectMapper exception
log.info("menuItemsData.json in project resource");
return null;
}
return rtnPageMenuItemModel;
}
/**
* 根據傳入的 PageMenuItemModel
* 物件,產生對應的系統選單資料並儲存至資料庫。
*
* 此方法會將傳入的根選單資料轉換為
* {@link tw.lewishome.webapp.database.primary.entity.SysMenuDataEntity},並設定相關屬性。
* 若根選單包含子選單項目,則會先清空所有現有選單資料,接著儲存根選單資料, 並遞迴將所有子選單項目加入資料庫。
*
*
* @param pageMenuItemModel 傳入的基礎選單項目模型,若為 {@code null} 則直接回傳 {@code null}
* @return 原始的 PageMenuItemModel 物件
*/
public PageMenuItemModel genSysMenuDataFromPageMenuItems(PageMenuItemModel pageMenuItemModel) {
if (pageMenuItemModel == null) {
return null;
}
// 轉換至 SysMenuDataEntity
SysMenuDataEntity sysMenuDataEntity = new SysMenuDataEntity();
SysMenuDataEntity.DataKey dataKey = new SysMenuDataEntity.DataKey();
sysMenuDataEntity.setDataKey(dataKey); // get root uuid
sysMenuDataEntity.setMenuSeq(0); // Set Root Menu Seq
sysMenuDataEntity.setIsMenu(true); // Set Root is Menu // not used
sysMenuDataEntity.setMenuDesc(pageMenuItemModel.getMenuBarText()); // Set Root Menu Desc as menuBarText
sysMenuDataEntity.setMenuUrl(""); // Set Root Menu Path
sysMenuDataEntity.setMenuFunc(""); // Set Root Menu Func
sysMenuDataEntity.setMenuParentUuid("-1"); // Set Root Menu Parent UUID
sysMenuDataEntity.setMenuRoles(""); // Set Root Menu Roles
// 處理 Child Menu Items
if (pageMenuItemModel.getNaviItems() != null && pageMenuItemModel.getNaviItems().size() > 0) {
sysMenuDataRepository.deleteAll();// 先刪除全部資料
sysMenuDataRepository.save(sysMenuDataEntity); // 先存Root Menu
List<PageMenuItemModel.MenuItems> listChildMenuItems = pageMenuItemModel.getNaviItems();
String oneMenuParentUuid = sysMenuDataEntity.getDataKey().getUuid(); // Child Menu Items的 Parent Uuid
for (int i = 0; i < listChildMenuItems.size(); i++) {
PageMenuItemModel.MenuItems oneChildMenuItems = listChildMenuItems.get(i);
addChildMenuItemToDB(oneChildMenuItems, oneMenuParentUuid);
}
}
return pageMenuItemModel;
}
private void addChildMenuItemToDB(PageMenuItemModel.MenuItems oneChildMenuItems, String oneMenuParentUuid) {
SysMenuDataEntity oneSysMenuDataEntity = new SysMenuDataEntity();
SysMenuDataEntity.DataKey oneDataKey = new SysMenuDataEntity.DataKey();
oneSysMenuDataEntity.setDataKey(oneDataKey); // get root uuid
oneSysMenuDataEntity.setMenuSeq(oneChildMenuItems.getMenuSeq()); // Set Root Menu Seq
oneSysMenuDataEntity.setIsMenu(oneChildMenuItems.getIsMenu()); // Set Root is Menu // not used
oneSysMenuDataEntity.setMenuDesc(oneChildMenuItems.getMenuDesc()); // Set Root Menu Desc as menuBarText
oneSysMenuDataEntity.setMenuUrl(oneChildMenuItems.getMenuUrl()); // Set Root Menu Path
oneSysMenuDataEntity.setMenuFunc(oneChildMenuItems.getMenuFunc()); // Set Root Menu Func
oneSysMenuDataEntity.setMenuParentUuid(oneMenuParentUuid); // Set Root Menu Parent UUID
oneSysMenuDataEntity.setMenuRoles(oneChildMenuItems.getMenuRoles()); // Set Root Menu Roles
sysMenuDataRepository.save(oneSysMenuDataEntity); // 存入 SysMenuDataEntity
List<PageMenuItemModel.MenuItems> listChildMenuItems = oneChildMenuItems.getCollapseItems();
if (oneChildMenuItems.getCollapseItems() != null && oneChildMenuItems.getCollapseItems().size() > 0) {
String childMenuParentUuid = oneSysMenuDataEntity.getDataKey().getUuid(); // Child Menu Items的 Parent Uuid
for (int i = 0; i < listChildMenuItems.size(); i++) {
PageMenuItemModel.MenuItems newChildMenuItems = listChildMenuItems.get(i);
addChildMenuItemToDB(newChildMenuItems, childMenuParentUuid);
}
}
}
/**
* 根據資料庫中的系統選單資料產生 PageMenuItemModel。
*
* 此方法會從資料庫取得 Root Menu(Parent Uuid = -1), 若找不到或有多個 Root Menu,則回傳 null 並顯示警告訊息。
* 若成功取得 Root Menu,則設定 menuBarText 及其子選單項目。
*
*
* @return 產生的 PageMenuItemModel,若無法正確取得 Root Menu 則回傳 null。
*/
public PageMenuItemModel genPageMenuItemsFromSysMenuData() {
PageMenuItemModel pageMenuItemModel = new PageMenuItemModel();
// 取得 Root Menu (Parent Uuid = -1)
List<SysMenuDataEntity> listRootMenu = sysMenuDataRepository.findByMenuParentUuid("-1");
if (listRootMenu == null || listRootMenu.size() == 0) {
log.info("Error: SysMenuDataEntity Root Menu not found");
return null;
} else {
if (listRootMenu.size() > 1) {
log.info("Error: Duplicate Root Menu SysMenuDataEntity > 1");
return null;
}
}
// Root Menu (Menu BarText)
SysMenuDataEntity rootMenu = listRootMenu.get(0);
pageMenuItemModel.setMenuBarText(rootMenu.getMenuDesc());
String rootMenuUuid = rootMenu.getDataKey().getUuid();
List<PageMenuItemModel.MenuItems> rootMenuItems = addChildMenuItemFromDB(listRootMenu.get(0), rootMenuUuid);
pageMenuItemModel.setNaviItems(rootMenuItems);
return pageMenuItemModel;
}
private List<PageMenuItemModel.MenuItems> addChildMenuItemFromDB(SysMenuDataEntity oneCSysMenuDataEntity,
String rootMenuUuid) {
List<PageMenuItemModel.MenuItems> rtnMenuItems = new ArrayList<>();
// 取得 parentMenuUuid SysMenuData
List<SysMenuDataEntity> listRootMenu = sysMenuDataRepository.findByMenuParentUuid(rootMenuUuid);
if (listRootMenu == null || listRootMenu.size() == 0) {
return null;
}
for (int i = 0; i < listRootMenu.size(); i++) {
SysMenuDataEntity oneChildSysMenuDataEntity = listRootMenu.get(i);
PageMenuItemModel.MenuItems oneChildMenuItems = new PageMenuItemModel.MenuItems();
oneChildMenuItems.setMenuSeq(oneChildSysMenuDataEntity.getMenuSeq());
oneChildMenuItems.setIsMenu(oneChildSysMenuDataEntity.getIsMenu());
oneChildMenuItems.setMenuDesc(oneChildSysMenuDataEntity.getMenuDesc());
oneChildMenuItems.setMenuUrl(oneChildSysMenuDataEntity.getMenuUrl());
oneChildMenuItems.setMenuFunc(oneChildSysMenuDataEntity.getMenuFunc());
oneChildMenuItems.setMenuRoles(oneChildSysMenuDataEntity.getMenuRoles());
String parentMenuUuid = oneChildSysMenuDataEntity.getDataKey().getUuid(); // Child Menu Items的 Parent Uuid
List<PageMenuItemModel.MenuItems> rootMenuItems = addChildMenuItemFromDB(oneChildSysMenuDataEntity,
parentMenuUuid);
if (rootMenuItems != null && rootMenuItems.size() > 0) {
oneChildMenuItems.setCollapseItems(rootMenuItems);
} else {
oneChildMenuItems.setCollapseItems(null);
}
rtnMenuItems.add(oneChildMenuItems);
}
return rtnMenuItems;
}
/**
* 將以分號(;)分隔的角色字串分割成清單,並將所有角色名稱轉換為大寫。
*
* @param pageMenuRoles 以分號分隔的角色名稱字串
* @return 轉換為大寫後的角色名稱清單
*/
public List<String> getListMenuRoles(String pageMenuRoles) {
List<String> rtnMenuRoles = new ArrayList<>();
String[] splitMenuRoles = pageMenuRoles.split(";");
for (int i = 0; i < splitMenuRoles.length; i++) {
rtnMenuRoles.add(splitMenuRoles[i]);
}
// 全部需要轉換大寫
rtnMenuRoles.replaceAll(String::toUpperCase);
return rtnMenuRoles;
}
/**
* 根據使用者名稱取得該使用者的所有角色清單。
*
* 此方法會依序執行下列步驟:
* <ul>
* <li>從 Spring Security 的 Authentication 物件取得使用者的權限角色。</li>
* <li>根據系統參數資料(如
* IT_ADMIN、SYS_ADMIN、USER_ADMIN、OP_ADMIN)判斷使用者是否擁有特殊角色,若有則加入角色清單。</li>
* <li>將使用者名稱(轉為大寫)作為一個角色加入清單。</li>
* <li>最後將所有角色名稱轉為大寫後回傳。</li>
* </ul>
*
* @param username 使用者名稱
* @return 包含所有角色名稱(皆為大寫)的清單
*/
private List<String> getUserRoles(String username) {
List<String> rtnUserRoles = new ArrayList<>();
// Add user roles from Authentication
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null) {
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
if (authorities.size() > 0) {
rtnUserRoles = getAuthoritiesRoles(authorities);
}
}
// Add special User Roles from SysUserProfileEntity
List<String> specialRoles = new ArrayList<>(Arrays.asList("IT_ADMIN", "SYS_ADMIN", "USER_ADMIN", "OP_ADMIN"));
for (int i = 0; i < specialRoles.size(); i++) {
String oneSpecialRole = specialRoles.get(i).trim();
if (hasSpecialRoles(username, oneSpecialRole)) {
rtnUserRoles.add(oneSpecialRole);
}
}
// Add User Name for one of Roles
rtnUserRoles.add(username.trim().toUpperCase());
// 全部需要轉換大寫
rtnUserRoles.replaceAll(String::toUpperCase);
return rtnUserRoles;
}
/**
* 取得使用者權限角色清單。
*
* 此方法會將傳入的 {@link GrantedAuthority} 集合中的每個權限字串進行處理, 取得去除 "ROLE_"
* 前綴後的角色名稱,若角色名稱包含 "Dept_",則會再進一步去除 "Dept_" 前綴。 最終回傳處理後的角色名稱字串清單。
*
*
* @param authorities 權限集合,來源為 Spring Security 的 {@link GrantedAuthority}
* @return 處理後的角色名稱字串清單
*/
private List<String> getAuthoritiesRoles(Collection<? extends GrantedAuthority> authorities) {
List<String> rtnUserRoles = new ArrayList<>();
authorities.forEach(x -> {
String authority = x.getAuthority();
String oneRole;
// 檢查是否以 "ROLE_" 開頭,如果是則移除前綴
if (authority.startsWith("ROLE_")) {
oneRole = authority.substring(5); // "ROLE_" 的長度是 5
} else {
oneRole = authority;
}
// 檢查是否以 "OIDC_" 開頭,如果是則移除前綴
if (authority.startsWith("OIDC_")) {
oneRole = authority; // OIDC_ 不移除
} else {
oneRole = authority;
}
// 如果角色名稱包含 "Dept_",則去除 "Dept_" 前綴
if (oneRole.contains("Dept_")) {
String[] parts = oneRole.split("_", 2); // 限制分割為最多 2 個部分
if (parts.length > 1) {
oneRole = parts[1];
}
}
rtnUserRoles.add(oneRole);
});
return rtnUserRoles;
}
/**
* 檢查指定使用者是否擁有特定的特殊角色。
*
* @param username 使用者名稱。
* @param oneSpecialRole 欲檢查的特殊角色參數名稱。
* @return 若使用者擁有該特殊角色則回傳 true,否則回傳 false。
*/
private Boolean hasSpecialRoles(String username, String oneSpecialRole) {
Boolean hasRoles = false;
List<String> listSysParmValue = getSysParmValueService.getSysParmDataEntityValue(oneSpecialRole);
for (int j = 0; j < listSysParmValue.size(); j++) {
String oneSysParmValue = listSysParmValue.get(j);
List<String> splitUsers = CommUtils.splitDelimiter(oneSysParmValue, ";");
if (splitUsers.contains(username)) {
hasRoles = true;
break;
}
}
return hasRoles;
}
}
PageControllerBase.java (new 前端服務處理Controller基本程式)package tw.lewishome.webapp.page.base.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.SessionAttributes;
// import org.springframework.web.util.WebUtils;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import tw.lewishome.webapp.page.base.model.PageMenuItemModel;
/**
* <pre>
* 提供共用的PageControllerBase 給所有Controller繼承。
* 基本包含 SessionAttributes ,以及設定Session 變數內容
* </pre>
*
* @author lewis
* @version $Id: $Id
*/
@Slf4j
@SessionAttributes({ "isLayoutTop", "menuDesc", "menuFunc", "pageButtonId",
"showAddBtn", "ajaxButtonString", "menuUrl", "headerDataString" })
public abstract class PageControllerBase {
/**
* Fix for javadoc warning :
* use of default constructor, which does not provide a comment
* Constructs a new BasePageController instance.
* This is the default constructor, implicitly provided by the compiler
* if no other constructors are defined.
*
*/
@Autowired
protected PageMenuItemService pageMenuItemService;
public PageControllerBase() {
// Constructor body (can be empty)
}
/**
* 設定Session 變數內容
*
* @param model Session Model
* @param request 前端給的 Request
*/
@ModelAttribute
public void addAttributes(Model model, HttpServletRequest request) {
log.info("PageControllerBase - addAttributes called - getRequestURI: ", request.getRequestURI());
// 增加動態 nonce變數內容到 Session變數
model.addAttribute("nonce", request.getAttribute("cspNonce"));
// WebUtils.getSessionAttribute(request, "currentUser");
// 取得 Login User
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String userId = null;
if (principal instanceof OAuth2User oauth2User) {
// OAuth2 授權使用 oauth2User 取得 userId (Email)
String email = (String) oauth2User.getAttribute("email");
userId = email != null ? email : oauth2User.getName();
} else if (principal instanceof UserDetails userDetails) {
// CustomAuthenticationProvider 授權使用 UserDetails 取得 userId
userId = userDetails.getUsername();
} else {
// 其他情況直接使用 principal 的 toString() 方法
userId = principal.toString();
}
model.addAttribute("userId", userId);
// 因為getMenuItems 有 Catch 所以 pageMenuItemModel 不用使用Session 變數
// anonymousUser (Security 白名單授權),不需要載入系統選單Menu
if (!userId.equalsIgnoreCase("anonymousUser")) {
// 依 Login User準備 Menu Items
PageMenuItemModel pageMenuItemModel = pageMenuItemService.getUserMenuItems(userId);
model.addAttribute("MenuItemModelVar", pageMenuItemModel); // 設定 Session 的 MenuItemModel
}
// 從 Session 取得 isLayoutTop
Boolean layoutTopVal = (Boolean) model.getAttribute("isLayoutTop");
if (layoutTopVal == null) { // Session 沒有 layoutTopVal 設為 false(SideAMenu)
model.addAttribute("isLayoutTop", true); // 設定 Session 的 layoutTopVal
}
// 從 Session 取得 menuFunc (是從Menu Item 點選的請求)
String menuFunc = (String) model.getAttribute("menuFunc");
model.addAttribute("menuFunc", menuFunc);
String menuUrl = (String) model.getAttribute("menuUrl");
model.addAttribute("menuUrl", menuUrl);
}
}
位置 package tw.lewishome.webapp.page;
PageConstants.java (new 前端服務處理的使用常變數)package tw.lewishome.webapp.page;
import tw.lewishome.webapp.page.base.model.PageMenuItemModel;
public class PageConstants {
/** Private constructor to prevent instantiation */
PageConstants() {
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated.");
}
/** 系統選單Menu 於由SystemApplicationListener 設定 */
public static PageMenuItemModel pageMenuItems = new PageMenuItemModel();
/** Spring Aspect PointCut Package 設定 */
public static final String ASPECT_PACKAGE = "execution(public * tw.lewishome.webapp.**page.controller.*.*(..))";
}
HomePageController.java (revise 增加URL處理程序)package tw.lewishome.webapp.page;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import jakarta.servlet.http.HttpServletRequest;
import tw.lewishome.webapp.GlobalConstants;
import tw.lewishome.webapp.base.utility.common.NetUtils;
import tw.lewishome.webapp.database.primary.entity.SysAccessLogEntity;
import tw.lewishome.webapp.database.primary.repository.SysAccessLogRepository;
import tw.lewishome.webapp.database.primary.service.SysUserProfileService;
import tw.lewishome.webapp.page.base.service.PageControllerBase;
import org.springframework.core.ResolvableType;
/**
* <pre>
* 系統跟目錄(Home)的控制服務程式
*
* 系統跟目錄(Home)的控制服務程式,包含以下主要功能 <br/>
* </pre>
*
* @author Lewis
* @version $Id: $Id
*/
@Controller // 宣告是 Controller 類別
public class HomePageController extends PageControllerBase {
/**
* Fix for javadoc warning :
* use of default constructor, which does not provide a comment
* Constructs a new HomePageController instance.
* This is the default constructor, implicitly provided by the compiler
* if no other constructors are defined.
*/
public HomePageController() {
// Constructor body (can be empty)
}
@Autowired
private SysAccessLogRepository sysAccessLogRepository;
@Autowired
SysUserProfileService sysUserProfileService;
private static String authorizationRequestBaseUri = "oauth2/authorization";
Map<String, String> oauth2AuthenticationUrls = new HashMap<>();
@Autowired
private ClientRegistrationRepository clientRegistrationRepository;
/**
* <pre>
* 接受前端 URL ==> "/home" or "/index" or "/的 Get request
* 顯示前端 /home/homePage.html
* </pre>
*
* @param model Session model
* @return String 前端Html
*/
@SuppressWarnings({ "unchecked", "null" })
@GetMapping({ "/home", "/index", "/" })
public String getHomeForm(Model model) {
// 回覆前端的 html ()
Iterable<ClientRegistration> clientRegistrations = null;
ResolvableType type = ResolvableType.forInstance(clientRegistrationRepository)
.as(Iterable.class);
if (type != ResolvableType.NONE &&
ClientRegistration.class.isAssignableFrom(type.resolveGenerics()[0])) {
clientRegistrations = (Iterable<ClientRegistration>) clientRegistrationRepository;
}
clientRegistrations.forEach(registration -> oauth2AuthenticationUrls.put(registration.getClientName(),
authorizationRequestBaseUri + "/" + registration.getRegistrationId()));
model.addAttribute("urls", oauth2AuthenticationUrls);
// 回覆前端的 html ()
return "/home/homePage.html";
}
/**
* 接受Login認證後 導向 "/landing" 的 getRequest
*
* @param model page Model
* @return String /home/landingPage.html
*/
@GetMapping({ "/landing" })
public String doLandingForm(Model model) {
model.addAttribute("menuFunc", "landing");
model.addAttribute("menuUrl", "landing");
return "/home/landingPage.html";
}
/**
* 處理 Menu點選的 URL (統一Menu入口,先處理Menu共同需要的程序)
*
* @param model Page Model
* @param request HttpServletRequest
* @param menuUrl 轉向 Menu 指定 URL
* @param menuDesc Menu說明 (功能 Title)
* @param menuFunc Menu功能 (查詢/維護)
* @return String menuUrl
*/
@GetMapping("doMenu")
public String doRedirectMenuUrl(Model model, HttpServletRequest request,
@RequestParam("Url") String menuUrl,
@RequestParam("Desc") String menuDesc,
@RequestParam("Func") String menuFunc) {
// html 會使用
model.addAttribute("menuDesc", menuDesc);
// html 會使用 在Post Endpoint時傳給後端使用
// model.addAttribute("isMenuUrl", true);
model.addAttribute("menuFunc", menuFunc);
model.addAttribute("menuUrl", menuUrl);
// headerDataString
model.addAttribute("headerDataString", "");
// SysAccessLogS
SysAccessLogEntity oneSAccessLogEntity = new SysAccessLogEntity();
SysAccessLogEntity.DataKey dataKey = new SysAccessLogEntity.DataKey();
oneSAccessLogEntity.setDataKey(dataKey);
oneSAccessLogEntity.setSessionId(request.getSession().getId());
oneSAccessLogEntity.setRemoteIp(NetUtils.getClientIpAddress(request));
oneSAccessLogEntity.setServerName(GlobalConstants.HOST_SERVER_NAME);
oneSAccessLogEntity.setAccessUrl(menuUrl + ":" + menuFunc);
String userId = (String) model.getAttribute("userId");
oneSAccessLogEntity.setAccessUser(userId);
oneSAccessLogEntity.setAction("Do Menu");
sysAccessLogRepository.saveAndFlush(oneSAccessLogEntity);
return "redirect:" + menuUrl;
}
/**
* 處理 "/chgMenuLayout" 的 Get request
*
* @param model Page Model
* @return String redirect 網址或前端Html
*/
@GetMapping({ "/chgMenuLayout" })
public String chgMenuLayout(Model model) {
// 從 Session 取得 isLayoutTop
Boolean layoutTopVal = (Boolean) model.getAttribute("isLayoutTop");
if (layoutTopVal == null) { // Session 沒有 layoutTopVal 設為 true(topMenu)
layoutTopVal = true;
}
layoutTopVal = !layoutTopVal; // layoutTopVal 做 not 運算切換 layoutTopVal
model.addAttribute("isLayoutTop", layoutTopVal); // 設定 Session 的 layoutTopVal
// 切換 Layout後,從 Session 取得當前 URL (menuUrl)
String menuUrl = (String) model.getAttribute("menuUrl");
if (StringUtils.isBlank(menuUrl)) { // 從 Session 沒有當前 URL (menuUrl)
menuUrl = "/"; // 設定當前URL (menuUrl) 為跟目錄
}
model.addAttribute("menuUrl", menuUrl);
return "redirect:" + menuUrl; // 轉到當前 URL (menuUrl)
}
}
**發文會切斷 ,接續 請參考 URL: (https://ithelp.ithome.com.tw/articles/10398905) **